Skip to content

Base interfaces & data structures#1

Open
grod220 wants to merge 9 commits intomainfrom
base-ixs
Open

Base interfaces & data structures#1
grod220 wants to merge 9 commits intomainfrom
base-ixs

Conversation

@grod220
Copy link
Copy Markdown
Member

@grod220 grod220 commented Apr 12, 2026

This PR adds base interfaces for the program: instructions, state, message schema, and PDA types.

Overall objective

Write a program that can serve as a functional replacement for the durable nonce usecase:

  • Approve something offline
  • Submit it later from a hot environment
  • Prevent replay
  • Support cold-signing and threshold-multisig workflows

The difference is that durable nonces do this via special runtime functionality (that folks are interested in removing), while this program does it through a promoting a PDA to signer on a pre-signed tx message.

Inspiration

Trent's durable nonce replacement proposal: solana-foundation/solana-improvement-documents#456

& Jon's idea to take a pre-signed-tx and promote the PDA to a signer on that tx.

High-level design

  • DurableSignerAccount: stores nonce Hash and an authority
  • DurableSignerPda: the PDA the program signs as during CPI
  • Submit instruction data: a serialized transaction payload

The intended flow is:

  1. Derive DurableSignerAccount from the authority and DurableSignerPda from the DurableSignerAccount.
  2. Initialize the account with the authority as a required signer.
  3. Build a wrapped transaction-v1 message whose lifetime specifier is state.nonce.
  4. Have every required wrapped signer authorize the canonical message bytes offline.
  5. A hot wallet submits Submit with that wrapped transaction as instruction data.
  6. Program then:
    a. Verifies the stored authority signed the wrapped message
    b. Checks message.lifetime_specifier == state.nonce
    c. Executes each wrapped instruction by CPI, promoting DurableSignerPda to signer wherever referenced
    d. Advances the nonce as sha256(tag ‖ state_pda ‖ old_nonce ‖ slot_hashes[0] ‖ sha256(message_bytes)).

Divergences from original proposal

This follows the spirit of Trent's original proposal, but there are a few divergences:

  • Does not use Vault naming as I felt it had too much overlap with the idea of custody and the defi vault concept.
  • Nonce is a 32-byte Hash advanced via a sha256 derivation and not a counter.
  • Signer seeds are not passed in the payload. The program derives one canonical authority PDA from the authority policy.

What's next

  • Add Codama support
  • Generate clients
  • Program implementation
  • Tests
  • CLI helpers
  • Benchmarking

@grod220 grod220 requested review from joncinque and t-nelson April 12, 2026 19:38
Copy link
Copy Markdown

@t-nelson t-nelson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good start!

Comment thread interface/src/message.rs Outdated
Comment thread interface/src/message.rs Outdated
Comment thread interface/src/message.rs Outdated
Comment thread interface/src/message.rs Outdated
Comment thread interface/src/message.rs Outdated
Comment thread interface/src/pda.rs Outdated
Comment thread interface/src/pda.rs Outdated
Comment thread interface/src/pda.rs Outdated
Comment thread interface/src/state.rs Outdated
Comment thread interface/src/state.rs Outdated
Copy link
Copy Markdown

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely on the right track!

We were talking about this offline, and I mentioned that a good use-case to target is minting SPL tokens, where the mint authority is a multisig, which I think is essentially Trent's sign(sign(sign(... example.

As another consideration, if people want to use tools like Ledgers as their offline signer, I don't think the current format will be accepted. We might need these to look like a Solana transaction instead, so that existing tools Just Work ™️

Comment thread interface/src/state.rs Outdated
Comment thread interface/src/state.rs Outdated
@t-nelson
Copy link
Copy Markdown

what's status here? foundation wants an example to float to current durable nonce consumers

@grod220
Copy link
Copy Markdown
Member Author

grod220 commented Apr 21, 2026

@t-nelson sorry for the delay on iterating on this. Talked offline to Jon a bit about a simpler design today. Expect an update soon.

@grod220
Copy link
Copy Markdown
Member Author

grod220 commented Apr 22, 2026

inspired by jon

This next iteration is an alternative design that simplifies the program a good deal. New flow:

  1. Consumer uses a standard signed solana_transaction::Transaction as the Submit payload.
  2. Nonce program validates: message.account_keys[0] == authority stored in PDA state && message.recent_blockhash == nonce in PDA state.
  3. Nonce program executes message.instructions by CPI, promoting NonceAuthorityPda to is_signer=true

It's no longer necessary to define a custom inner message format. This makes it friendlier to work with existing infra that expects signatures on the legacy transaction format.

Also removed embedded deadline/multisig support. Delegates that responsibility to external CPIs.

@grod220 grod220 requested review from joncinque and t-nelson April 22, 2026 01:27
Copy link
Copy Markdown

@t-nelson t-nelson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

making the payload (at least optionally) a transaction i think will ease some migration concerns. we'll need to think about how to handle some of the current features that are specified in transaction payloads, but require runtime support, which will not be available for these. similarly, how the message header will be handled

Comment thread interface/src/instruction.rs Outdated
Comment thread interface/src/instruction.rs Outdated
Comment thread interface/src/instruction.rs Outdated
Comment thread interface/src/instruction.rs Outdated
@grod220
Copy link
Copy Markdown
Member Author

grod220 commented Apr 30, 2026

@joncinque @t-nelson re-review on the updated API when you have a chance 🙏

Comment thread interface/src/state.rs
pub nonce: Hash,
/// Address allowed to consume this nonce and advance its value. `Submit` verifies that this
/// address signed the wrapped transaction message.
pub authority: Address,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we're enforcing a 1:1 mapping of authority to pda, this field is superfluous since it must be passed in the tx, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API update here: #1 (comment)

Think parallelism is a legitimate usecase requested from foundation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p sure it's the nonce hash that prevents "parallelism", not the authority


we need to tell these people that we're shipping an mvp replacement here, not taking feature requests

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An authority makes sense if we want to cover the existing use-case, where you have a nonce authority that must sign to advance the nonce, and then a bunch of other signers who are signers on the transaction.

A single authority can manage multiple nonce accounts for different offline signers. As a custodian, I might use the same authority for many different clients. When they want to move funds, they sign with one of my many accounts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure but we already have the authorities and the submit ix accounts list, as presently defined, only supports a single vault account. we can encode its authority in the ordering, like we do with the fee payer on outer tx, then assert it by virtue of the associated pda being passed. this could be trivially extended to multiple vault accounts should we choose with a vault count in the ix data

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm sure we're on the same page, there's two kinds of accounts: the PDA being promoted, and the account storing the hash and authority. For clarity, let's call the PDA the "vault" since it actually owns assets, and the other account the "durable signer".

In that sense, transactions must have at least one durable signer, and need to support having multiple vaults. Otherwise this program won't support existing use-cases, where multiple keypairs perform offline signing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the pubkey, address, account conflation in this ecosystem continues to pay out...

the account itself isn't really relevant to my point. sure it's stored at the authority's pda, but that pda is also the effective signer for inner payload execution. i don't see how it matters how many auxiliary programmatic signers we have on the inner payload, we only care about knowing which accounts index is the vault's to match the nonce stored there to what's signed on the inner payload. i'm proposing that we encode that information in the accounts list and forego duplicating the authority in the vault. so we'd have accounts indexes like this...

1. vault address
2. slothashes sysvar
3. instructions sysvar
4. vault authority pubkey
5. aux signer 1 pubkey
...
5+N. aux signer N pubkey
5+N+1. aux signer 1 pda
...
5+N+N. aux signer N pda
5+N+N+1. inner instruction account 1
...

at the cost of a (one byte?) aux signer count in the instruction payload

then the logic looks something like

// check sigs
vault = accounts[1]
vault_auth = accounts[4]
if (pda(vault_auth.address) == vault.address) {
  vault_data = VaultData::try_from(vault.data)?;
  if submit_ix_data.nonce == vault_data.nonce {
    // match aux signer[i] pda(accounts[5+i].address):accounts[5+N+i].address mappings
    // do the cpis
    ...
  }
}

i'm failing to see what storing the authority pubkey gains us

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this thread changes the names of the entities in this PR, it's an indication they probably need another pass (though "vault" is a specific defi primitive that could be a naming collision).

sure it's stored at the authority's pda
...
if (pda(vault_auth.address) == vault.address) {

I think there’s a model mismatch here. The account storing the nonce hash is not stored at the authority’s PDA.

This PR has two entities:

  • DurableSignerPda: the authority derived from cold-wallet keypair (the programmatic signer)
  • DurableSignerAccount: entity storing the nonce hash w/ an authority. An authority can own many and live at arbitrary addresses.

Without an authority listed in that account (and checked in program), anyone could increment that nonce and invalidate offline signed inner tx.

The reason where there is not a single DurableSignerAccount at a PDA of the authority is to support the cold-signer batch usecase where someone wants to cold-sign many at once (with multiple accounts -- so multiple nonce hashes).

Comment thread interface/Cargo.toml Outdated
homepage = {workspace = true}
license = {workspace = true}
edition = {workspace = true}
description = "Interface for the SPL Nonce program"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm thinking maybe we should just drop the "nonce" terminology altogether. any "programmatic signer/authority" can be used to decouple asset authority from signatures that must cover the recent blockhash

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Would spl-program-signer or spl-pda-authority or spl-authority-proxy be more accurate?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spl-ed25519-signer?

@joncinque join the bikeshed session!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been summoned! I really like including ed25519. On the other hand, this is still meant to cover single-use durable signatures, so how about spl-ed25519-durable-signer?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated terms!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there really isn't anything "durable" about this thing tho. that qualifier was added to the original feature as a call out to its extending the ttl of an otherwise short-lived nonce value. here we're simply never signing a nonce with a lifetime at all. this is so much more powerful and shouldn't be burdened by historical artifacts. the technology has advanced to the point as to make the old ways irrelevant. let them die


wherever we land, we should also rename the repo before it gets much content

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand -- "durable" means "long-lasting", and this program provides a long-lasting way to sign transactions. What are other powers that you want to highlight with the name?

Someone could make a different version of this program that requires signing the same blockhash as the outer transaction, which could be called ed25519-blockhash-signer. In this case, would ed25519-hash-signer be better? Or if you feel really strongly about spl-ed25519-signer, I can get behind it

Copy link
Copy Markdown

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is on the right path for me, I think implementation can get started very soon!

Comment thread interface/src/state.rs
pub nonce: Hash,
/// Address allowed to consume this nonce and advance its value. `Submit` verifies that this
/// address signed the wrapped transaction message.
pub authority: Address,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An authority makes sense if we want to cover the existing use-case, where you have a nonce authority that must sign to advance the nonce, and then a bunch of other signers who are signers on the transaction.

A single authority can manage multiple nonce accounts for different offline signers. As a custodian, I might use the same authority for many different clients. When they want to move funds, they sign with one of my many accounts

Comment thread interface/src/instruction.rs Outdated
Comment on lines +69 to +70
/// Runs only as an inner instruction of a wrapped transaction submitted through `Submit`
/// because nothing outside this program can sign for `NonceAuthorityPda`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clever! And makes sense, it's a great show of how this would work

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I found myself thinking through auth for this message and found the duplication annoying. Think this method addresses that!

/// 2. Reads the authority stored in the nonce state account.
/// 3. Checks the passed nonce state account's authority signed the wrapped message.
/// 4. Checks the wrapped message's lifetime / recent blockhash field equals `state.nonce`.
/// 5. Verifies the outer transaction's only top-level instruction is `Submit`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure about this restriction, but we can always relax it in the future if there are legitimate use-cases hampered by it

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the only real concern is that it makes v1 transactions a prerequisite for consumers that require compute budget instructions

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The program could potentially just skip compute budget instructions, no?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure. more shit to maintain in the program tho. more blast radius for upstream bugs. more shit to consider during deprecation. we're patching oob panic in v1 tx parsing rn. the more the program knows, the worse off it is

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trade off is
A - Hot wallet cannot insert ixs before/after inner tx and can only pass compute budget / priority fee params via the v1 transaction format.
vs
B - Hot wallet can insert ixs before or after. Inner tx doesn't have guarantees of what's run in the tx along with its CPIs.

It feels like B is safer, but I suppose the hot wallet is a trusted entity regardless as they could sandwich regardless via a jito bundle.

Comment thread interface/src/instruction.rs Outdated
Comment thread interface/src/instruction.rs Outdated
Comment on lines +61 to +62
/// - Remaining: all accounts referenced by the wrapped message, in order, with `is_signer`
/// and `is_writable` flags matching the wrapped message.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll see how this shakes out in the implementation, but it isn't possible for non-pda signers to appear in the original message, unless we expect them to sign the inner and outer transactions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. I suppose we could do something where a missing inner signature can be satisfied by outer transaction signer. That sort of would be a way to designate an intended hot wallet broadcaster. Hmm, may be best to come back to this during the implementation PR. Going to drop the is_signer language here for now.

@grod220 grod220 requested review from joncinque and t-nelson May 5, 2026 23:05
@joncinque
Copy link
Copy Markdown

This looks good to go from my side to start implementation, but please get @t-nelson 's approval

/// 2. Reads the authority stored in the nonce state account.
/// 3. Checks the passed nonce state account's authority signed the wrapped message.
/// 4. Checks the wrapped message's lifetime / recent blockhash field equals `state.nonce`.
/// 5. Verifies the outer transaction's only top-level instruction is `Submit`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure. more shit to maintain in the program tho. more blast radius for upstream bugs. more shit to consider during deprecation. we're patching oob panic in v1 tx parsing rn. the more the program knows, the worse off it is

Comment thread interface/src/state.rs
pub nonce: Hash,
/// Address allowed to consume this nonce and advance its value. `Submit` verifies that this
/// address signed the wrapped transaction message.
pub authority: Address,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the pubkey, address, account conflation in this ecosystem continues to pay out...

the account itself isn't really relevant to my point. sure it's stored at the authority's pda, but that pda is also the effective signer for inner payload execution. i don't see how it matters how many auxiliary programmatic signers we have on the inner payload, we only care about knowing which accounts index is the vault's to match the nonce stored there to what's signed on the inner payload. i'm proposing that we encode that information in the accounts list and forego duplicating the authority in the vault. so we'd have accounts indexes like this...

1. vault address
2. slothashes sysvar
3. instructions sysvar
4. vault authority pubkey
5. aux signer 1 pubkey
...
5+N. aux signer N pubkey
5+N+1. aux signer 1 pda
...
5+N+N. aux signer N pda
5+N+N+1. inner instruction account 1
...

at the cost of a (one byte?) aux signer count in the instruction payload

then the logic looks something like

// check sigs
vault = accounts[1]
vault_auth = accounts[4]
if (pda(vault_auth.address) == vault.address) {
  vault_data = VaultData::try_from(vault.data)?;
  if submit_ix_data.nonce == vault_data.nonce {
    // match aux signer[i] pda(accounts[5+i].address):accounts[5+N+i].address mappings
    // do the cpis
    ...
  }
}

i'm failing to see what storing the authority pubkey gains us

///
/// Accounts required:
/// - `[writable]` Durable signer account whose nonce is consumed and advanced
/// - `[]` `SlotHashes` sysvar
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we confirm whether or not this is in fact "deprecated"? i recall coming across deprecation attributes and deprecation allows wrt it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1 (comment)

Think you are thinking of RecentBlockhashes.

SlotHashes is currently available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants